Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | import { getDb } from "@/lib/db";
import { ShareController } from "@/server/api/shares";
import { ResultController } from "@/server/api/results";
import { ResultItemController } from "@/server/api/result-items";
import { ItemController } from "@/server/api/items";
import { PhotoController } from "@/server/api/photos";
import { getSharedResultPhotoAssetUrl, PHOTO_WIDTH } from "@/lib/photos";
import { Badge, Card, EmptyState } from "@/components/ui";
import { ItemCard } from "@/features/items/ItemCard";
import { notFound } from "next/navigation";
interface SharedResultPageProps {
params: Promise<{ token: string }>;
}
export default async function SharedResultPage({ params }: SharedResultPageProps) {
const { token } = await params;
const db = getDb();
const share = await ShareController.fromDb(db).findByToken(token);
if (!share || share.status !== "active" || (share.expiresAt && share.expiresAt < new Date())) {
notFound();
}
const result = await ResultController.fromDb(db).getResultOrNull(share.sortResultId);
if (!result || result.status !== "finalized") {
notFound();
}
const [resultItems, items] = await Promise.all([
ResultItemController.fromDb(db).listResultItems(result.id),
ItemController.fromDb(db).listItems(result.collectionId),
]);
const itemById = new Map(items.map((item) => [item.id, item]));
const sortedResultItems = [...resultItems].sort((a, b) => (a.manualRank ?? a.computedRank) - (b.manualRank ?? b.computedRank));
// Anonymous viewer: photo bytes must come from the token-scoped route, never
// the session-gated /api/photo-files one (purses-21x5.1) — a signed-out
// caller would 401 on every image.
const covers = await PhotoController.fromDb(db).findCoverPhotosByItemIds(
sortedResultItems.map((ri) => ri.itemId),
);
const heroPhotoUrl = (itemId: string) => {
const cover = covers.get(itemId);
return cover ? getSharedResultPhotoAssetUrl(token, cover, { w: PHOTO_WIDTH.card }) : undefined;
};
return (
<main className="min-h-screen px-4 py-6 sm:py-8">
<div className="mx-auto max-w-6xl space-y-8">
<div>
<div className="mb-3">
<Badge variant="finalized">finalized</Badge>
</div>
<h1 className="text-2xl text-ink sm:text-3xl">{result.name}</h1>
<p className="mt-2 text-ink-soft">Shared read-only result</p>
</div>
<Card padding="lg">
{sortedResultItems.length === 0 ? (
<EmptyState heading="No ranked items" message="This shared result does not contain ranked items yet" />
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{sortedResultItems.map((resultItem) => {
const item = itemById.get(resultItem.itemId);
if (!item) return null;
return (
<ItemCard
key={resultItem.id}
item={item}
collectionId={result.collectionId}
heroPhotoUrl={heroPhotoUrl(item.id)}
bucket={resultItem.manualBucket || resultItem.computedBucket}
/>
);
})}
</div>
)}
</Card>
</div>
</main>
);
}
|